package net.letcode.core.utils.sign;

import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import net.letcode.core.utils.common.DataMap;
import net.letcode.core.utils.common.StringUtils;
import net.letcode.core.utils.security.Encodes;
import net.letcode.core.utils.security.rsa.RSA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 签名与校验工具
 * @author ZDF
 * @date 2015年3月6日
 *
 */
public class SignUtils {
	
	/** 日志记录器 */
	public static Logger logger = LoggerFactory.getLogger(SignUtils.class);
	
	/** 签名类型：默认类型 */
	public static final SignType TYPE_DEFAULT = SignType.RSA;
	
	/** 不需要签名的字段 */
	private static final List<String> UNSIGN_FIELD = Arrays.asList("sign", "sign_type", "signType");
	
	/** 京东PC端支付：不需要签名的字段 */
	private static final List<String> JD_PC_UNSIGN_FIELD = Arrays.asList("sign", "merchantSign", "version", "successCallbackUrl", "forPayLayerUrl");
	
	/** 京东移动端支付：不需要签名的字段 */
	private static final List<String> JD_WAP_UNSIGN_FIELD = Arrays.asList("sign", "merchantSign", "version", "token");
	
	/** 排序比较器 */
	private static final Comparator<String> comparator = new Comparator<String>() {
		@Override
		public int compare(String o1, String o2) {
			return o1.compareTo(o2);
		}
	};
	
	/**
	 * 对参数进行签名，默认使用RSA签名协议
	 * @author ZDF
	 * @date 2015年6月22日
	 * @param parameterMap
	 * @param signKey
	 * @return
	 * 		校验通过时返回排序后的参数集合，并追加签名参数
	 */
	public static String signMap(Map<String, Object> parameterMap, String signKey) {
		return signMap(parameterMap, signKey, TYPE_DEFAULT);
	}
	
	/**
	 * 对参数进行签名
	 * @param parameterMap 待签名参数
	 * @param signKey	签名密钥
	 * @param type 		签名类型
	 * @return
	 */
	public static String signMap(Map<String, Object> parameterMap, String signKey, SignType type) {
		String signValue = null;
		
		Map<String, Object> dataMap = new TreeMap<String, Object>(comparator);
		byte[] sourceData;
		byte[] encryptData;
		
		try {
			String key = null;
			String value = null;
			StringBuffer sb = new StringBuffer();
			switch (type) {
			case MD5:
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key))
						sb.append(key).append("=").append(value).append("&");
			    }
				sb.append("key=").append(signKey);
				signValue = Encodes.md5(sb.toString()).toUpperCase();
				break;
			case RSA:
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key))
						sb.append(key).append("=").append(value).append("&");
			    }
				sb.deleteCharAt(sb.length() - 1);
				
				// 对源字符串进行SHA散列处理，避免字符串过长导致兼容、性能问题
				String message = sb.toString();
				message = Encodes.sha256(message);
				signValue = RSA.signByPrivateKey(message, signKey);
				break;
			case TB_MD5:
				sb.append(signKey);
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key))
						sb.append(key).append(value);
			    }
				sb.append(signKey);
				logger.debug("淘宝API签名字符串：{}", sb);
				signValue = Encodes.md5(sb.toString()).toUpperCase();
				logger.debug("淘宝API签名结果：{}", signValue);
				break;
			case ZFB_MD5:
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key) && StringUtils.isNotBlank(value))
						sb.append(key).append("=").append(value).append("&");
			    }
				sb.deleteCharAt(sb.length() - 1);
				sb.append(signKey);
				signValue = Encodes.md5(sb.toString());
				break;
			case ZFB_RSA_PC:
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key) && StringUtils.isNotBlank(value))
						sb.append(key).append("=").append("\"").append(value).append("\"").append("&");
			    }
				sb.deleteCharAt(sb.length() - 1);
				
				signValue = RSA.signByPrivateKey(sb.toString(), signKey);
				signValue = URLEncoder.encode(signValue, "UTF-8");
				break;
			case ZFB_RSA_WAP:
				dataMap.putAll(parameterMap);
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!UNSIGN_FIELD.contains(key) && StringUtils.isNotBlank(value))
						sb.append(key).append("=").append(value).append("&");
			    }
				sb.deleteCharAt(sb.length() - 1);
				
				signValue = RSA.signByPrivateKey(sb.toString(), signKey);
				break;
			case JD_MD5:
				dataMap.putAll(parameterMap);
				sb.append(dataMap.get("v_amount"))
				  .append(dataMap.get("v_moneytype"))
				  .append(dataMap.get("v_oid"))
				  .append(dataMap.get("v_mid"))
				  .append(dataMap.get("v_url"))
				  .append(signKey);
				signValue = Encodes.md5(sb.toString()).toUpperCase();
				break;
			case JD_RSA_PC:
				dataMap.putAll(parameterMap);
				try {
					for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
						key =  entry.getKey();
						value = String.valueOf(entry.getValue());
						if(!JD_PC_UNSIGN_FIELD.contains(key))
							sb.append(key).append("=").append(value).append("&");
				    }
					sb.deleteCharAt(sb.length() - 1);
					sourceData = Encodes.sha256(sb.toString().getBytes("UTF-8"));
					encryptData = RSA.encryptByPrivateKey(sourceData, StringUtils.getBytesBase64(signKey));
					signValue = StringUtils.getStringBase64(encryptData);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			case JD_RSA_WAP:
				dataMap.putAll(parameterMap);
				try {
					for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
						key =  entry.getKey();
						value = String.valueOf(entry.getValue());
						if(!JD_WAP_UNSIGN_FIELD.contains(key))
							sb.append(key).append("=").append(value).append("&");
				    }
					sb.deleteCharAt(sb.length() - 1);
					sourceData = Encodes.sha256(sb.toString().getBytes("UTF-8"));
					sourceData = StringUtils.getBytesUtf8(Encodes.hexEncode(sourceData));
					encryptData = RSA.encryptByPrivateKey(sourceData, StringUtils.getBytesBase64(signKey));
					signValue = StringUtils.getStringBase64(encryptData);
				} catch (Exception e) {
					e.printStackTrace();
				}
				break;
			default:
				break;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return signValue;
	}

	/**
	 * 校验MAP参数的签名，默认使用RSA签名协议
	 * @author ZDF
	 * @date 2015年6月22日
	 * @param parameterMap
	 * @param signKey
	 * @return
	 * 			校验通过时返回排序后的参数集合，失败是返回null
	 */
	public static DataMap checkMap(Map<String, Object> parameterMap, String signKey) {
		return checkMap(parameterMap, signKey, TYPE_DEFAULT);
	}
	
	/**
	 * 校验MAP参数的签名
	 * @author ZDF
	 * @date 2015年6月22日
	 * @param parameterMap
	 * @param signKey
	 * @param type
	 * @return
	 */
	public static DataMap checkMap(Map<String, Object> parameterMap, String signKey, SignType type) {
		Boolean pass =  false;
		
		Map<String, Object> dataMap = new TreeMap<String, Object>(comparator);
		dataMap.putAll(parameterMap);
	
		String key = null;
		String signValue = null;
		String sign = null;
		String value = null;
		StringBuffer sb = new StringBuffer();
		
		switch (type) {
		case MD5:
			for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
				key = entry.getKey();
				value = String.valueOf(entry.getValue());
				if(!UNSIGN_FIELD.contains(key) && (value != null))
					sb.append(key).append("=").append(value).append("&");
			}
			sb.append("key=").append(signKey);
			
			signValue = Encodes.md5(sb.toString()).toUpperCase();
			sign = (String) dataMap.get("sign");
			
			if (signValue.equals(sign))
				pass = true;
			
			break;
		case RSA:
			for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
				key =  entry.getKey();
				value = String.valueOf(entry.getValue());
				if(!UNSIGN_FIELD.contains(key)  && (value != null))
					sb.append(key).append("=").append(value).append("&");
		    }
			sb.deleteCharAt(sb.length() - 1);
			
			// 对源字符串进行SHA散列处理，避免字符串过长导致兼容、性能问题
			signValue = sb.toString();
			signValue = Encodes.sha256(signValue);
			
			sign = (String) dataMap.get("sign");
			if(StringUtils.isNotBlank(sign))
				pass = RSA.verifyByPublicKey(signValue, signKey, sign);
		case ZFB_MD5:
			for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
				key =  entry.getKey();
				value = String.valueOf(entry.getValue());
				if(!UNSIGN_FIELD.contains(key) && StringUtils.isNotBlank(value))
					sb.append(key).append("=").append(value).append("&");
		    }
			sb.deleteCharAt(sb.length() - 1);
			sb.append(signKey);
			
			signValue = Encodes.md5(sb.toString());
			sign = (String) dataMap.get("sign");
			
			if (signValue.equals(sign))
				pass = true;
			
			break;
		case ZFB_RSA_PC:
			for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
				key =  entry.getKey();
				value = String.valueOf(entry.getValue());
				if(!UNSIGN_FIELD.contains(key) && StringUtils.isNotBlank(value))
					sb.append(key).append("=").append(value).append("&");
		    }
			sb.deleteCharAt(sb.length() - 1);
			
			sign = (String) dataMap.get("sign");
			pass = RSA.verifyByPublicKey(sb.toString(), signKey, sign);
			break;
		case JD_MD5:
			sb.append(dataMap.get("v_oid"))
			  .append(dataMap.get("v_pstatus"))
			  .append(dataMap.get("v_amount"))
			  .append(dataMap.get("v_moneytype"))
			  .append(signKey);
			
			signValue = Encodes.md5(sb.toString()).toUpperCase();
			sign = (String) dataMap.get("v_md5str");
			
			if (signValue.equals(sign))
				pass = true;
			
			break;
		case JD_RSA_PC:
			try {
				for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
					key =  entry.getKey();
					value = String.valueOf(entry.getValue());
					if(!JD_PC_UNSIGN_FIELD.contains(key)  && (value != null))
						sb.append(key).append("=").append(value).append("&");
			    }
				sb.deleteCharAt(sb.length() - 1);
				signValue = sb.toString();
				String signLocal = Encodes.sha256(signValue);
				
				sign = (String) dataMap.get("sign");
				String signSource = RSA.decryptByPublicKey(sign, signKey);
				if (signSource.equals(signLocal))
					pass = true;
			} catch (Exception e) {
				e.printStackTrace();
			}
			break;
		default:
			break;
		}
		
		if (pass)
			return new DataMap(dataMap);
		else{
			logger.warn("签名校验失败：\nSRC: {}\nDES: {}", sign, signValue);
			return null;
		}
	}

	/**
	 * 校验XML参数的签名，默认使用RSA签名协议
	 * @author ZDF
	 * @date 2015年6月22日
	 * @param xml
	 * @param signKey
	 * @return
	 * 			校验通过时返回排序后的参数集合，失败是返回null
	 */
	public static DataMap checkXml(String xml, String signKey) {
		return checkXml(xml, signKey, TYPE_DEFAULT);
	}
	
	/**
	 * 校验XML参数的签名
	 * @author ZDF
	 * @date 2015年6月22日
	 * @param xml
	 * @param signKey
	 * @param type
	 * @return
	 */
	public static DataMap checkXml(String xml, String signKey, SignType type) {
		Map<String, Object> dataMap = null;
		try {
			Document doc = DocumentHelper.parseText(xml);
			Map<String, Object> parameterMap = new TreeMap<String, Object>();
			Element item = null;
			Element rootElement = doc.getRootElement();
			Iterator<Element> iterator = rootElement.elementIterator();
			while(iterator.hasNext()){
				item = iterator.next();
				parameterMap.put(item.getName(), item.getText());
			}
			
			dataMap = new TreeMap<String, Object>(comparator);
			dataMap.putAll(parameterMap);
			
			String key = null;
			String value = null;
			StringBuffer sb = new StringBuffer();
			for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
				key =  entry.getKey();
				value = String.valueOf(entry.getValue());
				if(!("sign".equals(key) || "sign_type".equals(key)) && (value != null))
					sb.append(key).append("=").append(value).append("&");
			}
			
			switch (type) {
			case MD5:
				sb.append("key=").append(signKey);
				break;
			case ZFB_MD5:
				sb.append(signKey);
				break;
			default:
				break;
			}
			
			String signValue = Encodes.md5(sb.toString()).toUpperCase();
			String sign = (String) dataMap.get("sign");
			
			if(!signValue.equals(sign)){
				logger.warn("签名校验失败：\nSRC: {}\nDES: {}", sign, signValue);
				dataMap = null;
			}
		} catch (DocumentException e) {
			dataMap = null;
			e.printStackTrace();
		}
		
		return new DataMap(dataMap);
	}
}
